/*
 * Copyright (c) 2010, Nikolaus Moll
 * All rights reserved.
 */
package net.miblounge.gradecalculator.gui;

import java.io.File;
import java.util.LinkedList;
import java.util.List;

import net.miblounge.gradecalculator.core.AbstractDataProvider;
import net.miblounge.gradecalculator.core.AdaptedRegulations;
import net.miblounge.gradecalculator.core.AdaptedRegulationsUtil;
import net.miblounge.gradecalculator.core.descriptors.StudyRegulationsDescription;
import net.miblounge.gradecalculator.core.descriptors.courses.Course;
import net.miblounge.gradecalculator.core.descriptors.courses.CourseDescription;
import net.miblounge.gradecalculator.core.descriptors.modulelistitems.IndividualModule;
import net.miblounge.gradecalculator.core.descriptors.modulelistitems.Module;
import net.miblounge.gradecalculator.core.enums.CourseType;
import net.miblounge.gradecalculator.core.enums.ExaminationType;
import net.miblounge.gradecalculator.core.student.ExaminationResult;
import net.miblounge.gradecalculator.core.student.Student;
import net.miblounge.gradecalculator.core.student.StudentModule;
import net.miblounge.gradecalculator.dataprovider.DataProvider;
import net.miblounge.gradecalculator.exceptions.XMLException;
import net.miblounge.gradecalculator.gui.dialogs.AbstractResultDialog;
import net.miblounge.gradecalculator.gui.dialogs.ResultDialogFactory;
import net.miblounge.gradecalculator.gui.dialogs.StudentDialog;
import net.miblounge.gradecalculator.gui.models.GradesTreeModel;
import net.miblounge.gradecalculator.guicommon.dialogs.CommonDialogs;
import net.miblounge.gradecalculator.guicommon.dialogs.CommonDialogs.ChangedDialogResult;
import net.miblounge.gradecalculator.guicommon.interfaces.IMutliColumnTreeEventListener;
import net.miblounge.gradecalculator.guicommon.util.ActionUtil;
import net.miblounge.gradecalculator.guicommon.widgets.MultiColumnTree;
import net.miblounge.gradecalculator.guicommon.widgets.TreeIndex;
import net.miblounge.gradecalculator.importplugin.data.ImportedExaminationResult;
import net.miblounge.gradecalculator.importplugin.interfaces.ICommonImportPlugin;
import net.miblounge.gradecalculator.importplugin.interfaces.IFileImportPlugin;
import net.miblounge.gradecalculator.importplugin.interfaces.IImportPlugin;
import net.miblounge.gradecalculator.plugin.core.util.ImportPluginUtil;
import net.miblounge.gradecalculator.plugin.core.util.ImportPluginUtil.PluginDescriptor;
import net.miblounge.gradecalculator.plugin.core.util.ResultConvert;
import net.miblounge.gradecalculator.plugin.exceptions.PluginException;

import com.trolltech.qt.core.QSize;
import com.trolltech.qt.gui.QAction;
import com.trolltech.qt.gui.QApplication;
import com.trolltech.qt.gui.QCloseEvent;
import com.trolltech.qt.gui.QDialog;
import com.trolltech.qt.gui.QFileDialog;
import com.trolltech.qt.gui.QMainWindow;
import com.trolltech.qt.gui.QMenu;
import com.trolltech.qt.gui.QMessageBox;
import com.trolltech.qt.gui.QMessageBox.StandardButton;
import com.trolltech.qt.gui.QToolBar;
import com.trolltech.qt.gui.QWidget;

/* 
 * TODO: 
 * - Context-Menues
 * - Check-Boxes (wie persistentEditor)
 * - Grid zeichnen, Modul-Rahmen
 * 
 */

public class GradeCalculator extends QMainWindow implements IMutliColumnTreeEventListener
{
    private GradesTreeModel model;
    private final MultiColumnTree spo;
    private AdaptedRegulations studentSPO = null;
    private String openedFile = null;
    private int studentResultHash = 0;

    private QAction actFileNew;
    private QAction actFileOpen;
    private QAction actFileSave;
    private QAction actFileSaveAs;
    private QAction actEditStudent;
    private QAction actEditResult;
    private QAction actNewCourse;
    private QAction actDeleteCourse;
    private QAction actViewOpenAllUnfinished;

    private Object currentTreeItem = null;

    public GradeCalculator(final QWidget parent)
    {
        super(parent);
        DataProvider.initialize();
        ImportPluginUtil.initialize();
        for (final Throwable throwable : ImportPluginUtil.getExceptions()) {
            showException(throwable);
        }

        createStatusBar();
        createActions();
        createToolBars();
        createMenus();

        spo = new MultiColumnTree(this);
        spo.addTreeEventListener(this);
        setCentralWidget(spo);

        // TODO: REMOVE TEST CODE
        final File file = new File("./resources/nikolausmoll.stux");
        if (file.exists()) {
            openFile(file.getAbsolutePath());
        }
        else {
            setOpenedFile(null);
        }
    }

    private void createStatusBar()
    {
        statusBar().showMessage(tr("Ready"));
    }

    private void createActions()
    {
        actFileNew = ActionUtil.createAction(this, tr("&New"), "actionNew()", tr("Create a new student"));
        actFileOpen = ActionUtil.createAction(this, tr("&Open"), "actionOpen()", tr("Open a new student file"));
        actFileSave = ActionUtil.createAction(this, tr("&Save"), "actionSave()", tr("Saves the current file"));
        actFileSaveAs = ActionUtil.createAction(this, tr("Save &as"), "actionSaveAs()",
                tr("Saves the current fild with a new name"));
        actEditStudent = ActionUtil.createAction(this, tr("Student"), "actionStudent()", tr("Setup current student"));
        actNewCourse = ActionUtil.createAction(this, tr("Add course"), "actionAddCourse()", tr("Adds a new course"));
        actDeleteCourse = ActionUtil.createAction(this, tr("Delete course"), "actionDeleteCourse()",
                tr("Deletes a course"));
        actEditResult = ActionUtil.createAction(this, tr("Edit result"), "actionEditResult()", tr("Change the result"));

        actViewOpenAllUnfinished = ActionUtil.createAction(this, tr("Open all &unfinsihed modules"),
                "actionOpenUnfinished()", tr("Expands all modules with unfinished courses"));
    }

    private void createToolBars()
    {
        final QToolBar toolbar = addToolBar(tr("Toolbar"));
        toolbar.addAction(actFileNew);
        toolbar.addAction(actFileOpen);
        toolbar.addAction(actFileSave);
        toolbar.addAction(actFileSaveAs);
        toolbar.addSeparator();
        toolbar.addAction(actNewCourse);
        toolbar.addAction(actDeleteCourse);
        toolbar.addSeparator();
        toolbar.addAction(actEditResult);
    }

    protected void setOpenedFile(final String filename)
    {
        String title = tr("Grade Calculator") + " " + ProgramDescription.getVersion();
        if (filename != null) {
            openedFile = filename;
            String usedname = filename;
            usedname = usedname.replace("\\", "/");
            final int index = usedname.lastIndexOf('/');
            if (index >= 0) {
                usedname = usedname.substring(index + 1);
            }
            title += " - " + usedname;
            studentResultHash = studentSPO.getStudent().hashCode();
        }
        else {
            openedFile = null;
            studentResultHash = 0; // reset hash
        }
        this.setWindowTitle(title);
        updateActions();
    }

    public void openFile(final String filename)
    {
        try {
            ExaminationType.reset(); // backup?
            CourseType.reset(); // backup?
            final Student student = Student.createFromXML(filename);
            final StudyRegulationsDescription spod = AbstractDataProvider.getInstance().getSPODescription(
                    student.getSPODescriptionName());
            studentSPO = AdaptedRegulationsUtil.createSPO(spod, student);

            model = new GradesTreeModel(this, studentSPO);
            spo.setModel(model);
            setOpenedFile(filename);
        }
        catch (final PluginException e) {
            showException(e);
        }
        catch (final XMLException e) {
            showException(e);
        }
    }

    protected boolean saveStudent(final String filename)
    {
        if (studentSPO == null) {
            return true;
        }

        final String saveFilename;
        if (filename == null) {
            saveFilename = CommonDialogs.getSaveAsFilename(this, "Save student file as", "Student file", "stux");
            if (saveFilename == null) {
                return false;
            }
        }
        else {
            saveFilename = filename;
        }

        try {
            studentSPO.getStudent().saveXML(saveFilename);
            setOpenedFile(saveFilename);
            return true;
        }
        catch (final XMLException e) {
            showException(e);
            return false;
        }
    }

    protected boolean isChanged()
    {
        if ((studentSPO == null) || (studentResultHash == studentSPO.getStudent().hashCode())) {
            return false;
        }
        return true;
    }

    protected ChangedDialogResult checkClose()
    {
        if (!isChanged()) {
            return ChangedDialogResult.CONTINUE;
        }

        final StandardButton action = CommonDialogs.getFileChangedAction(this);
        if (action == StandardButton.Cancel) {
            return ChangedDialogResult.CANCEL;
        }

        if (action == StandardButton.Save) {
            if (!saveStudent(openedFile)) {
                return ChangedDialogResult.CANCEL;
            }
        }

        return ChangedDialogResult.CONTINUE;
    }

    protected void actionOpen()
    {
        if (checkClose() == ChangedDialogResult.CANCEL) {
            return;
        }

        final String filename = CommonDialogs.getOpenFilename(this, "Open student file", "Student file", "stux");
        if (filename != null) {
            openFile(filename);
        }
    }

    protected void actionNew()
    {
        if (checkClose() == ChangedDialogResult.CANCEL) {
            return;
        }

        final Student student = new Student();
        final StudentDialog dialog = new StudentDialog(this, student);
        final int result = dialog.exec();
        if (result == QDialog.DialogCode.Accepted.ordinal()) {
            dialog.apply();

            final StudyRegulationsDescription spod = AbstractDataProvider.getInstance().getSPODescription(
                    student.getSPODescriptionName());
            studentSPO = AdaptedRegulationsUtil.createSPO(spod, student);
            model = new GradesTreeModel(this, studentSPO);
            spo.setModel(model);
            setOpenedFile(null);
        }
    }

    protected void actionSave()
    {
        saveStudent(openedFile);
    }

    protected void actionSaveAs()
    {
        saveStudent(null);
    }

    protected void actionExit()
    {
        close();
    }

    protected void actionEditResult()
    {
        final Object item = spo.getSelectedItem();

        if (item instanceof Course) {
            final AbstractResultDialog dialog = ResultDialogFactory.create(this, studentSPO, (Course) item);
            final int dlgresult = dialog.exec();

            if (dlgresult == QDialog.DialogCode.Accepted.ordinal()) {
                dialog.apply();
                model.notifyDataChanged(spo.getCurrentTreeIndex());
            }
        }

        //
        // if (item.getTag() instanceof ExaminationResult) {
        // // TODO: implement (urgent)
        // QMessageBox.information(parentWidget(), "Information",
        // "Sorry, not implemented yet");
        // }
    }

    protected void actionOpenUnfinished()
    {
        // spo.openAllUnfinished();
    }

    protected void actionStudent()
    {
        Student student;
        if (studentSPO != null) {
            student = studentSPO.getStudent();
        }
        else {
            student = new Student();
        }
        final StudentDialog dialog = new StudentDialog(this, student);
        final int result = dialog.exec();
        if (result == QDialog.DialogCode.Accepted.ordinal()) {
            dialog.apply();

            final StudyRegulationsDescription spod = AbstractDataProvider.getInstance().getSPODescription(
                    student.getSPODescriptionName());
            studentSPO = AdaptedRegulationsUtil.createSPO(spod, student);

            model = new GradesTreeModel(this, studentSPO);
            spo.setModel(model);
            updateActions();
        }
    }

    protected void actionImport(final QAction sender)
    {
        ICommonImportPlugin plugin = null;
        for (final PluginDescriptor pd : ImportPluginUtil.getPlugins()) {
            if (pd.getName().equals(sender.text())) {
                plugin = pd.getPlugin();
                break;
            }
        }

        if (plugin == null) {
            // TODO: ErrorMessage
            return;
        }

        try {
            if (plugin instanceof IFileImportPlugin) {
                final IFileImportPlugin filePlugin = (IFileImportPlugin) plugin;

                final StringBuilder extensions = new StringBuilder();
                for (final String extension : filePlugin.getExtensions()) {
                    extensions.append("*." + extension + " ");
                }

                // example:
                // String filter =
                // "Images (*.png *.xpm *.jpg);;Text files (*.txt);;XML files (*.xml)";
                final String filter = tr("Supported files") + " ( " + extensions.toString().trim() + ");;"
                        + tr("All files") + " (*.*)";

                final String filename = QFileDialog.getOpenFileName(this, tr("Open file"), System.getenv("user.dir"),
                        new QFileDialog.Filter(filter));

                if ((filename == null) || (filename.equals(""))) {
                    return;
                }

                filePlugin.startImport(filename);
            }
            else if (plugin instanceof IImportPlugin) {
                final IImportPlugin importPlugin = (IImportPlugin) plugin;
                importPlugin.startImport();
            }
        }
        catch (final Exception e) {
            showException(e);
            return;
        }

        final Student s = new Student();
        s.setName(plugin.getStudent());
        s.setUniversityName(plugin.getUniversity());
        s.setDegree(plugin.getDegree());
        s.setCourseOfStudies(plugin.getCourseOfScience());
        s.setSemester(plugin.getSemester().replace(" ", ""));

        final StudentDialog dialog = new StudentDialog(this, s);
        final int result = dialog.exec();
        if (result != QDialog.DialogCode.Accepted.ordinal()) {
            return;
        }

        dialog.apply();

        final StudyRegulationsDescription spod = AbstractDataProvider.getInstance().getSPODescription(
                s.getSPODescriptionName());

        for (final ImportedExaminationResult importedResult : plugin.getResults()) {
            final ExaminationResult examinationResult = ResultConvert.convertImportedResult(spod, importedResult);
            s.addResult(examinationResult);
        }

        studentSPO = AdaptedRegulationsUtil.createSPO(spod, s);

        model = new GradesTreeModel(this, studentSPO);
        spo.setModel(model);
        setOpenedFile(null);

        updateActions();
    }

    private void createImportMenu(final QMenu menuImport)
    {
        menuImport.triggered.connect(this, "actionImport(QAction)");
        for (final PluginDescriptor pd : ImportPluginUtil.getPlugins()) {
            final QAction action = new QAction(pd.getName(), null);
            action.setStatusTip(pd.getPlugin().getStatusLineText());
            // TODO: set toolltip
            menuImport.addAction(action);
        }

    }

    private void createMenus()
    {
        final QMenu menuFile = menuBar().addMenu(tr("&File"));
        menuFile.addAction(actFileNew);
        menuFile.addAction(actFileOpen);

        menuFile.addAction(actFileSave);
        menuFile.addAction(actFileSaveAs);
        menuFile.addSeparator();
        if (ImportPluginUtil.getPlugins().size() > 0) {
            createImportMenu(menuFile.addMenu(tr("&Import")));
        }
        else {
            final QAction importAction = ActionUtil.createAction(this, tr("&Import"), "",
                    "No importer plugins detected");
            importAction.setEnabled(false);
            menuFile.addAction(importAction);
        }
        menuFile.addSeparator();
        menuFile.addAction(ActionUtil.createAction(this, tr("&Exit"), "actionExit()", tr("Exits the programm")));

        final QMenu menuEdit = menuBar().addMenu(tr("&Edit"));
        menuEdit.addAction(actNewCourse);
        menuEdit.addAction(actEditResult);
        menuEdit.addSeparator();
        menuEdit.addAction(actEditStudent);

        final QMenu menuView = menuBar().addMenu(tr("&View"));
        menuView.addAction(actViewOpenAllUnfinished);

        // menuHelp = menuBar().addMenu(tr("&Help"));
        // menuHelp.addAction(actAbout);
        // menuHelp.addAction(actAboutQtJambi);
    }

    public static List<String> getExceptionText(final Throwable throwable, final boolean isCause)
    {
        final List<String> result = new LinkedList<String>();
        final String title = throwable.getClass().toString() + ": " + throwable.getMessage();
        if (isCause) {
            result.add("Caused by: " + title);
        }
        else {
            result.add(title);
        }
        for (final StackTraceElement ste : throwable.getStackTrace()) {
            result.add(ste.toString());
        }

        if (throwable.getCause() != null) {
            result.addAll(getExceptionText(throwable.getCause(), true));
        }

        return result;
    }

    private boolean isNewCourseAvailable()
    {
        if (currentTreeItem == null) {
            return false;
        }

        if (currentTreeItem instanceof StudentModule) {
            return ((StudentModule) currentTreeItem).getModule() instanceof IndividualModule;
        }

        if (currentTreeItem instanceof CourseDescription) {
            return true;
        }

        return false;
    }

    protected void updateActions()
    {
        actFileSave.setEnabled(isChanged());
        actFileSaveAs.setEnabled(studentSPO != null);

        actNewCourse.setEnabled(isNewCourseAvailable());
        actDeleteCourse.setEnabled(currentTreeItem instanceof CourseDescription
                || currentTreeItem instanceof ExaminationResult);
        actEditResult.setEnabled(currentTreeItem instanceof Course);
    }

    @Override
    protected void closeEvent(final QCloseEvent event)
    {
        if (checkClose() == ChangedDialogResult.CANCEL) {
            event.ignore();
        }
        else {
            event.accept();
        }
    }

    @Override
    public void treeSelectionChangedEvent(final TreeIndex index, final Object data)
    {
        currentTreeItem = data;
        updateActions();
    }

    @Override
    public void treeEditRequestEvent(final TreeIndex index, final int column, final Object data)
    {
        if (data instanceof Course) {
            final boolean useInlineEditor;
            if (model.isInlineEditorAllowed(index, column)) {
                useInlineEditor = false; // TODO: fix crash when pressing
                                         // enter/escape in editor
            }
            else {
                useInlineEditor = false;
            }

            if (useInlineEditor) {
                spo.startInlineEditor(index, column);
            }
            else {
                final AbstractResultDialog dialog = ResultDialogFactory.create(this, studentSPO, (Course) data);
                final int dlgresult = dialog.exec();

                if (dlgresult == QDialog.DialogCode.Accepted.ordinal()) {
                    dialog.apply();
                    model.notifyDataChanged(spo.getCurrentTreeIndex());

                    updateActions();
                }
            }
        }
    }

    protected void actionAddCourse()
    {
        final IndividualModule module;
        if (currentTreeItem instanceof StudentModule) {
            final Module commonModule = ((StudentModule) currentTreeItem).getModule();
            if (!(commonModule instanceof IndividualModule)) {
                return;
            }
            module = (IndividualModule) commonModule;
        }
        else {
            if (!(currentTreeItem instanceof CourseDescription)) {
                return;
            }
            module = ((CourseDescription) currentTreeItem).getParentModule();
        }

        final CourseDescription description = new CourseDescription();
        description.setParentModule(module);

        final AbstractResultDialog dialog = ResultDialogFactory.create(this, studentSPO, description);
        final int dlgresult = dialog.exec();

        if (dlgresult == QDialog.DialogCode.Accepted.ordinal()) {
            dialog.apply();
            model.addCourse(description);

            updateActions();
        }
    }

    protected void actionDeleteCourse()
    {
        if (currentTreeItem instanceof CourseDescription) {
            final CourseDescription course = (CourseDescription) currentTreeItem;
            model.removeCourse(course);
            updateActions();
        }

        if (currentTreeItem instanceof ExaminationResult) {
            final ExaminationResult examinationResult = (ExaminationResult) currentTreeItem;
            model.removeExaminationResult(examinationResult);
            updateActions();
        }
    }

    public void showException(final Throwable throwable)
    {
        final List<String> details = getExceptionText(throwable, false);
        final int lines = Math.min(details.size(), 20);
        final StringBuilder message = new StringBuilder();
        for (int i = 0; i < lines; i++) {
            message.append(details.get(i) + "\n");
        }
        if (details.size() > lines) {
            message.append("... " + (details.size() - lines) + " more");
        }

        QMessageBox.critical(this, tr("Error"), message.toString());
    }

    public static void main(final String[] args)
    {
        QApplication.initialize(args);

        final GradeCalculator window = new GradeCalculator(null);
        window.setMinimumSize(new QSize(640, 480));
        window.show();

        QApplication.exec();
    }

}
